Return generic error messages to clients for server-side errors#530
Return generic error messages to clients for server-side errors#530ChristianPavilonis wants to merge 5 commits intomainfrom
Conversation
58c5906 to
820599c
Compare
prk-Jr
left a comment
There was a problem hiding this comment.
Summary
This hardens client-facing error bodies and the overall direction looks right. GitHub CI is passing, but I found a couple of follow-up improvements around classification drift and regression coverage.
Non-blocking
🤔 thinking
- InvalidUtf8 is currently surfaced as Invalid request data, but its only producer today is embedded config decoding in settings_data.rs, so it still reads like a client error for a server bootstrap failure.
♻️ refactor
- status_code() and user_message() now encode the exposure policy in separate matches. Centralizing that mapping behind a small helper or enum would make it harder for classification and response text to drift apart.
aram356
left a comment
There was a problem hiding this comment.
@ChristianPavilonis Please resolve conflicts
Replace user_message() passthrough of Display output with a match that returns generic messages for 5xx/502/503 errors while keeping brief descriptions for 4xx client errors. Full error details are already logged via log::error! and never lost. Closes #437
0e8dc86 to
b955ece
Compare
aram356
left a comment
There was a problem hiding this comment.
Summary
Well-scoped PR that prevents server-side error details from leaking to clients. The secure-by-default wildcard pattern (new variants automatically get the generic message) is the right approach for a security boundary.
Blocking
❓ question
InvalidUtf8classification: Only produced bysettings_data.rs:24(server bootstrap), yet classified as a 4xx client error returning "Invalid request data". Should this fall through to the generic message? (crates/trusted-server-core/src/error.rs:131)GdprConsentmessage suppression: The only 4xx error that fully hides its detail message. The rationale (consent strings may contain user data) is sound, but confirming intent and adding a doc comment on the variant would help future maintainers. (crates/trusted-server-core/src/error.rs:127)
Non-blocking
🤔 thinking
status_code()anduser_message()can drift apart: Both methods encode client-vs-server classification in separatematchblocks. If a new variant is classified as 4xx instatus_code()but hits the wildcard inuser_message(), it would return 400 with "An internal error occurred" — confusing but safe by default. Not a concern for this PR, but worth noting.
⛏ nitpick
BadRequestdoc comment precision: Says "message is returned to clients" but the message is formatted viaformat!("Bad request: {message}"), not returned verbatim. (crates/trusted-server-core/src/error.rs:20)
🏕 camp site
- PR body references old crate path
crates/common/src/error.rs— the crate was renamed totrusted-server-corein PR #517. - PR checklist says "Uses
tracingmacros" but per CLAUDE.md the project uses thelogcrate.
CI Status
- fmt: PASS
- clippy: PASS
- rust tests: PASS
- js tests: PASS
- format checks: PASS
aram356
left a comment
There was a problem hiding this comment.
Summary
Security-focused change that replaces the user_message() passthrough (which exposed internal error details like store names, config paths, proxy addresses) with categorized responses: client errors (4xx) get brief safe descriptions, server errors (5xx/502/503) get a generic message. Well-scoped single-file change with good test coverage.
Non-blocking
🤔 thinking
- Wildcard match may silently suppress future client errors: The
_ =>catch-all inuser_message()defaults new variants to the generic message, even if they're 4xx client errors. Consider matching exhaustively to force explicit decisions. (error.rs:134)
♻️ refactor
BadRequestformat string duplicated:user_message()and#[display]both hardcode"Bad request: {message}"— could diverge silently. (error.rs:128)
🌱 seedling
- Consider a compile-time guarantee that
status_code()anduser_message()stay in sync: Both methods independently classify variants as client vs. server errors. A future follow-up could extract the classification into a single method (e.g.,is_client_error()) or match exhaustively in both to prevent divergence.
📝 note
InvalidUtf8status code change is correct: The 400 → 500 change is appropriate — the only usage is for the embedded TOML settings file, which is a server-side resource.
CI Status
- fmt: PASS
- clippy: PASS
- rust tests: PASS
- js tests: PASS
- integration tests: PASS
- browser integration tests: PASS
- CodeQL: PASS
| Self::InvalidHeaderValue { .. } => "Invalid header value".to_string(), | ||
| // Server/integration errors (5xx/502/503) — generic message only. | ||
| // Full details are already logged via log::error! in to_error_response. | ||
| _ => "An internal error occurred".to_string(), |
There was a problem hiding this comment.
🤔 thinking — Wildcard match may silently suppress future client errors
The _ => catch-all means any new variant added to TrustedServerError will default to the generic "An internal error occurred" message — even if it's a 4xx client error. This is safe-by-default (no leaks), but it could silently give users unhelpful messages for legitimate client errors.
Consider matching exhaustively (no _) so the compiler forces an explicit decision when new variants are added. The status_code() method already matches exhaustively, so user_message() would stay consistent.
Alternatively, the existing test server_errors_return_generic_message partially mitigates this — but only if someone remembers to add new variants to the test.
| self.to_string() | ||
| match self { | ||
| // Client errors (4xx) — safe to surface a brief description | ||
| Self::BadRequest { message } => format!("Bad request: {message}"), |
There was a problem hiding this comment.
♻️ refactor — BadRequest format string duplicated between Display and user_message()
user_message() returns format!("Bad request: {message}") which duplicates the #[display("Bad request: {message}")] attribute. If one changes, the other could silently diverge.
Suggestion: Reuse Display for the BadRequest arm:
Self::BadRequest { .. } => self.to_string(),| Self::InsecureDefault { .. } => StatusCode::INTERNAL_SERVER_ERROR, | ||
| Self::InvalidHeaderValue { .. } => StatusCode::BAD_REQUEST, | ||
| Self::InvalidUtf8 { .. } => StatusCode::BAD_REQUEST, | ||
| Self::InvalidUtf8 { .. } => StatusCode::INTERNAL_SERVER_ERROR, |
There was a problem hiding this comment.
📝 note — InvalidUtf8 status code change from 400 → 500 is correct
The only usage is for the embedded trusted-server.toml file (settings_data.rs:24), which is a server-side resource. Invalid UTF-8 there is a server error, not a client error.
aram356
left a comment
There was a problem hiding this comment.
Summary
Clean, focused security fix that correctly addresses #437. The user_message() method previously exposed internal details (store names, IP addresses, config paths) to clients. Now server/integration errors (5xx/502/503) return a generic string while client errors (4xx) retain safe descriptions.
Non-blocking
🌱 seedling
status_code/user_messageconsistency test: The wildcard_arm inuser_message()catches everything with the generic message. If someone adds a new 4xx variant tostatus_codebut forgets to updateuser_message, it would silently get the 500-style generic message. A test that asserts all 4xx variants have a specific (non-generic)user_messagewould catch this.
📝 note
- Tests use
.into()for String construction: CLAUDE.md prefersType::from(value)over.into()for clarity. Common Rust idiom in tests though — not worth blocking on.
👍 praise
- Correct reclassification of
InvalidUtf8from 400 to 500 - Thoughtful suppression of
GdprConsentdetails (consent strings may contain user data) - Comprehensive test coverage of all error variants
CI Status
All 13 checks: PASS
| fn user_message(&self) -> String { | ||
| // Use the Display implementation which already has the specific error message | ||
| self.to_string() | ||
| match self { |
There was a problem hiding this comment.
🤔 thinking — BadRequest passes caller-provided strings directly to clients
The BadRequest variant echoes its message field verbatim to clients. While the doc comment warns callers to keep it free of internal details, there's no compile-time or runtime enforcement. Current call sites are safe, but as more are added a caller could accidentally pass sensitive details. A future refinement (e.g., a newtype ClientMessage(String) or a fixed enum of safe messages) could prevent this.
| /// return a generic message. Full error details are preserved in server logs. | ||
| fn user_message(&self) -> String; | ||
| } | ||
|
|
There was a problem hiding this comment.
👍 praise — Correct reclassification of InvalidUtf8 to 500
The only usage is in settings_data.rs for the embedded TOML config — a server-side data integrity issue, not client input. Changing from BAD_REQUEST to INTERNAL_SERVER_ERROR is the right call.
| @@ -30,6 +34,10 @@ pub enum TrustedServerError { | |||
| #[display("GAM error: {message}")] | |||
There was a problem hiding this comment.
👍 praise — Thoughtful handling of GdprConsent
Suppressing detail for consent errors (consent strings may contain user data) while still returning the category name is a well-reasoned middle ground. Good doc comment explaining the rationale.
| self.to_string() | ||
| match self { | ||
| // Client errors (4xx) — safe to surface a brief description | ||
| Self::BadRequest { message } => format!("Bad request: {message}"), |
There was a problem hiding this comment.
🏕 camp site — InvalidHeaderValue message could include the header name
Since this is a 400 (client error), including which header was invalid would help debugging. Current usage in cookies.rs has "Cookie header contains invalid UTF-8" in the message field, but that detail is now suppressed by the match. Safe and correct as-is — just a campsite opportunity.
Summary
user_message()passthrough with categorized responsesChanges
crates/trusted-server-core/src/error.rsself.to_string()inuser_message()with amatchthat surfaces safe messages for client errors and a generic message for server errors. Add doc comments onBadRequest(user-visible message warning),GdprConsent(why detail is suppressed), anduser_messagetrait method (updated semantics). Add two unit tests covering all variants.Closes
Closes #437
Test plan
cargo test --workspacecargo clippy --all-targets --all-features -- -D warningscargo fmt --all -- --checkcd crates/js/lib && npx vitest runcd crates/js/lib && npm run formatcd docs && npm run formatcargo build --bin trusted-server-fastly --release --target wasm32-wasip1fastly compute serveChecklist
unwrap()in production code — useexpect("should ...")logmacros (notprintln!)